/**
 * getFolderSize
 *
 * Get the size of a folder.
 *
 * Referring to get-folder-size, @link https://github.com/alessioalex/get-folder-size
 */

import {join} from "node:path";
import fs from "node:fs";

/**
 * Configuration options.
 */
export interface FolderSizeOptions {
  /**
   * Should the folder size be returned as a BigInt instead of a Number.
   */
  bigint?: boolean;
  /**
   * If a file's path matches this regex object, its size is not counted.
   */
  ignore?: RegExp;
}

export interface ReturnType {
  /**
   * If true, errors will be thrown instead of returned.
   */
  strict?: boolean;
  /**
   * If true, errors will be returned.
   */
  errors?: boolean;
}

class FolderSize {

  public size(itemPath: string, options?: FolderSizeOptions) {
    return this.core(itemPath, options, {errors: true});
  }

  public loose(itemPath: string, options?: FolderSizeOptions & {
    bigint?: boolean | undefined;
  }): Promise<number>;
  public loose(itemPath: string, options?: FolderSizeOptions & {
    bigint: true;
  }): Promise<bigint>;

  public loose(itemPath: string, options?: FolderSizeOptions) {
    return this.core(itemPath, options);
  }

  public strict(itemPath: string, options?: FolderSizeOptions & {
    bigint?: boolean | undefined;
  }): Promise<number>;
  public strict(itemPath: string, options?: FolderSizeOptions & {
    bigint: true;
  }): Promise<bigint>;

  public strict(itemPath: string, options?: FolderSizeOptions) {
    return this.core(itemPath, options, {strict: true});
  }

  private async core(rootItemPath: string, options: FolderSizeOptions = {}, returnType: ReturnType = {}) {
    let folderSize: bigint | number = 0n;
    const foundInos = new Set();
    const errors = [];
    if (!fs.existsSync(rootItemPath)) return 0;

    const fsp = fs.promises;

    await processItem(rootItemPath);

    async function processItem(itemPath: string) {
      if (options.ignore?.test(itemPath)) return;

      const stats = returnType.strict
        ? await fsp.lstat(itemPath, {bigint: true})
        : await fsp
          .lstat(itemPath, {bigint: true})
          .catch((error) => errors.push(error));
      if (typeof stats !== "object") return;
      if (!foundInos.has(stats.ino)) {
        foundInos.add(stats.ino);
        // @ts-ignore
        folderSize += stats.size;
      }

      if (stats.isDirectory()) {
        const directoryItems = returnType.strict
          ? await fsp.readdir(itemPath)
          : await fsp
            .readdir(itemPath)
            .catch((error) => errors.push(error));
        if (typeof directoryItems !== "object") return;
        await Promise.all(
          directoryItems.map((directoryItem) =>
            processItem(join(itemPath, directoryItem)),
          ),
        );
      }
    }

    if (!options.bigint) {
      if (folderSize > BigInt(Number.MAX_SAFE_INTEGER)) {
        const error = new RangeError(
          "The folder size is too large to return as a Number. You can instruct this package to return a BigInt instead.",
        );
        if (returnType.strict) {
          throw error;
        }
        errors.push(error);
        folderSize = Number.MAX_SAFE_INTEGER;
      } else {
        folderSize = Number(folderSize);
      }
    }

    if (returnType.errors) {
      return {
        size: folderSize,
        errors: errors.length > 0 ? errors : null,
      };
    } else {
      return folderSize;
    }
  }
}

export default new FolderSize();
